perf: sharded OnceTable - #225
Conversation
|
Verified on bare-metal Linux: AMD Ryzen 7 5700X (8C/16T), NixOS 26.11, kernel 7.2.0, rustc 1.96.0, t=8,
The two approaches look complementary: sharding covers disjoint/churn, a shared reader path plus the hit shortcut from 6c55ca4 covers same-key. A per-shard reader path on top of this PR should cover every scenario — happy to help with that. |
|
Refactor the storage of OnceMap, add a concurrent index for Ready values. Also fixed some issues in benchmarks, but the results still show excessive relative range.
New Result: 1 thread
2 threads
8 threads
32 threads
|
a6b45fb to
9d1ee47
Compare
| } | ||
|
|
||
| if entry.was_indexed.load(Ordering::Relaxed) { | ||
| self.index.remove_if_sync(&EntryIdentity(entry), |()| true); |
There was a problem hiding this comment.
[P1] Drop entries when they are discarded
HashIndex::remove_if_sync only marks an entry unreachable and explicitly permits its memory to be reclaimed later. Because ReadyEntry owns a strong Arc<Entry<K, V>>, removing the entry from the shard does not release its key or value. The following integration test fails on this head with left: 0, right: 1, while it passes against 0.6.7:
#[test]
fn discard_drops_the_removed_value() {
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use asyncband::once::OnceMap;
#[derive(Clone)]
struct DropCounter(Arc<AtomicUsize>);
impl Drop for DropCounter {
fn drop(&mut self) {
self.0.fetch_add(1, Ordering::SeqCst);
}
}
let drops = Arc::new(AtomicUsize::new(0));
let map: OnceMap<_, _> =
[(0, DropCounter(Arc::clone(&drops)))].into_iter().collect();
map.discard(&0);
assert_eq!(drops.load(Ordering::SeqCst), 1);
}This is the documented reclamation behavior of HashIndex::remove_if_sync, but it is observable here because the deferred index entry owns the map's key and value transitively. Under insert/discard churn, resources can accumulate until the index collector—or the whole OnceMap—is dropped. Please avoid retaining a strong Arc in the deferred-reclamation index, or otherwise ensure that strong ownership is released before discard returns.
| // So I use 8 as the coefficient, which is 256 / 32. | ||
| // Need to test on other machines to see if this coefficient is optimal. | ||
| // Dashmap use 4. | ||
| (std::thread::available_parallelism().map_or(1, |parallelism| parallelism.get()) * 8) |
There was a problem hiding this comment.
[P2] Bound or expose the eager shard allocation
Eager CPU-scaled sharding has precedent, but this exact default is unusually aggressive for OnceMap and especially singleflight::Group. DashMap uses available_parallelism * 4, eagerly creates its shards, and exposes with_shard_amount constructors so callers can trade contention for footprint. The canonical Go singleflight implementation instead lazily initializes one mutex-protected map, and scc::HashIndex, already used by this PR, starts with zero capacity.
This implementation uses available_parallelism * 8 with no override. On this 14-way host that becomes 128 #[repr(align(64))] shards. An allocator probe measured:
OnceMap::new()andGroup::new(): 1 allocation / 8,192 bytes, versus 0 allocations in 0.6.7.OnceMap::with_capacity(1): 132 allocations / 14,592 bytes, versus 1 allocation / 44 bytes in 0.6.7.
On the 32-core machine used to choose the coefficient, every empty instance starts with 256 shards and at least 16 KiB of cache-line-padded storage. The throughput results establish the benefit for a hot shared table, but do not cover construction or workloads containing many empty/small maps and groups.
Could we add construction/allocation benchmarks and either expose an explicit shard-count constructor with a more conservative or capped default, or allocate shard storage lazily? The sharding optimization itself is justified; the concern is making its most aggressive configuration an unavoidable per-instance cost.
|
Removed additional dependencies, use std::RwLock instead of scc::HashIndex: 32 threads
|
Signed-off-by: tison <wander4096@gmail.com>
Signed-off-by: tison <wander4096@gmail.com>
Alternative of #205
Improvements
CPU: AMD Ryzen 9 9950X
OS: NixOS 25.11
Thread: 8
Thread: 32